package com.koelo.kiss.re.parser;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import com.koelo.kiss.re.interpreter.IInterpreter;
import com.koelo.kiss.re.util.IContext;
import com.koelo.kiss.re.util.IMessage;
import com.koelo.kiss.re.util.IRule;
import com.koelo.kiss.re.util.ISession;

public class KissParser 
	implements IParser {

	private Logger LOGGER = Logger.getLogger(KissParser.class);
	private String source;
	
	public KissParser(String source) {
		
		this.source = source;
	}

	public KissParser(InputStream inputStream) {
		
		this(new Scanner(inputStream).useDelimiter("\\Z").next());
	}

	public KissParser(File source) 
		throws FileNotFoundException {
		
		this(new FileInputStream(source));
	}
	
	public IRule init(IInterpreter interpreter) {

		try {
			DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
			DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder();
			Document document = documentBuilder.parse(new InputSource(new StringReader(this.source)));
			return parse(interpreter, document);
		} catch (ParserConfigurationException ex) {
			throw new ParserException(ex);
		} catch (SAXException ex) {
			throw new ParserException(ex);
		} catch (IOException ex) {
			throw new ParserException(ex);
		}
	}

	public IRule parse(IInterpreter interpreter, Document document) {
		
		Element root = document.getDocumentElement();
		Rule rule = parseRule(interpreter, root);
		for (int i = 0; i < root.getChildNodes().getLength(); i++) {
			Node node = root.getChildNodes().item(i);
			if (node.getNodeType() == Node.ELEMENT_NODE) {
				if (node.getNodeName().equals("rule")) {
					parseRule(interpreter, rule, node);
				} else if (node.getNodeName().equals("include")) {
					parseInclude(interpreter, rule, node);
				} else {
					throw new ParserException("Invalid node name [" + node.getNodeName() + "]. Expected [rule] or [include] node.");
				}
			}
		}
		return rule;
	}

	private Rule parseRule(IInterpreter interpreter, Node node) {

		if (!node.getNodeName().equals("rule")) {
			throw new ParserException("Invalid node name [" + node.getNodeName() + "]. Expected [rule] node.");
		}
		if (node.getAttributes().getNamedItem("name") == null) {
			throw new ParserException("Cannot find attribute [name] for node [rule].");
		} else if (node.getAttributes().getNamedItem("name").getNodeValue().trim().equals("")) {
			throw new ParserException("Invalid empty value for attribute [name] and node [rule].");
		}
		Rule rule = new Rule(node.getAttributes().getNamedItem("name").getNodeValue().trim()); 
		boolean matchFirst = false;
		if (node.getAttributes().getNamedItem("matchFirst") != null) {
			matchFirst = Boolean.parseBoolean(node.getAttributes().getNamedItem("matchFirst").getNodeValue().trim());
		}
		rule.setMatchFirst(matchFirst);
		if (node.getAttributes().getNamedItem("class") != null) {
			if (node.getAttributes().getNamedItem("name").getNodeValue().trim().equals("")) {
				throw new ParserException("Invalid empty value for attribute [class] and node [rule].");
			}
			rule.setRule(interpreter.getResource(node.getAttributes().getNamedItem("class").getNodeValue().trim()));
		}
		for (int i = 0; i < node.getChildNodes().getLength(); i++) {
			Node childNode = node.getChildNodes().item(i);
			if (childNode.getNodeType() == Node.ELEMENT_NODE) {
				if (childNode.getNodeName().equals("args")) {
					parseArgs(interpreter, rule, childNode);
				} else if (childNode.getNodeName().trim().equals("rule")) {
					parseRule(interpreter, rule, childNode);
				} else if (childNode.getNodeName().trim().equals("include")) {
					parseInclude(interpreter, rule, childNode);
				} else {
					throw new ParserException("Invalid node name [" + childNode.getNodeName() + "]. Expected [args] or [rule] or [include] node.");
				}
			}
		}
		return rule;
	}
	
	private void parseRule(IInterpreter interpreter, Rule parent, Node node) {

		parent.addAction(parseRule(interpreter, node));
	}
	
	private void parseArgs(IInterpreter interpreter, IRule parent, Node node) {

		Map<String, Object> args = new HashMap<String, Object>();
		for (int i = 0; i < node.getChildNodes().getLength(); i++) {
			Node childNode = node.getChildNodes().item(i);
			if (childNode.getNodeType() == Node.ELEMENT_NODE) {
				if (!childNode.getNodeName().equals("arg")) {
					throw new ParserException("Invalid node name [" + node.getNodeName() + "]. Expected [arg] node.");
				}
				if (childNode.getAttributes().getNamedItem("name") == null) {
					throw new ParserException("Cannot find attribute [name] for node [arg].");
				} else if (childNode.getAttributes().getNamedItem("name").getNodeValue().trim().equals("")) {
					throw new ParserException("Invalid empty value for attribute [name] and node [arg].");
				}
				if (childNode.getAttributes().getNamedItem("value") == null) {
					throw new ParserException("Cannot find attribute [value] for node [arg].");
				}
				String name = childNode.getAttributes().getNamedItem("name").getNodeValue().trim();
				String value = childNode.getAttributes().getNamedItem("value").getNodeValue();
				if (value.startsWith("${") && value.endsWith("}")) {
					args.put(name, interpreter.getContext().getVariable(value.substring(2, value.length() - 1)));
				} else {
					args.put(name, value);
				}
			}
		}
		((Rule) parent).setArgs(args);
	}

	private void parseInclude(IInterpreter interpreter, Rule parent, Node node) {

		if (!node.getNodeName().equals("include")) {
			throw new ParserException("Invalid node name [" + node.getNodeName() + "]. Expected [include] node.");
		}
		if (node.getAttributes().getNamedItem("file") == null) {
			throw new ParserException("Cannot find attribute [file] for node [include].");
		} else if (node.getAttributes().getNamedItem("file").getNodeValue().trim().equals("")) {
			throw new ParserException("Invalid empty value for attribute [file] and node [include].");
		}
		try {
			parent.addAction(new KissParser(new File(node.getAttributes().getNamedItem("file").getNodeValue().trim())).init(interpreter));
		} catch (FileNotFoundException ex) {
			throw new ParserException(ex.getMessage(), ex);
		}
	}
	
	private class Rule
		implements IRule {

		private List<IRule> actions;
		private Map<String, Object> args;
		private IRule rule;
		private String name;
		private boolean matchFirst;

		public Rule(String name, IRule rule) {

			this.actions = new ArrayList<IRule>();
			this.args = new HashMap<String, Object>();
			setName(name);
			setRule(rule);
		}

		public Rule(String name) {
			
			this(name, null);
		}

		public void setName(String name) {
			
			this.name = name;
		}
		
		public String getName() {
			
			return this.name;
		}
		
		public void setMatchFirst(boolean matchFirst) {
			
			this.matchFirst = matchFirst;
		}
		
		public boolean isMatchFirst() {
			
			return this.matchFirst;
		}
		
		public void setRule(IRule rule) {
			
			this.rule = rule;
		}
		
		public IRule getRule() {
			
			return this.rule;
		}
		
		public void setArgs(Map<String, Object> args) {
			
			this.args = args;
		}
		
		public Map<String, Object> getArgs() {
			
			return this.args;
		}

		public void addAction(IRule action) {
			
			this.actions.add(action);
		}
		
		public List<IRule> getActions() {
			
			return this.actions;
		}
		
		@Override
		public boolean eval(IContext context, ISession session, IMessage message) {
	
			LOGGER.info("Evaluating rule [" + getName() + "] (start)");
			boolean result = true;
			if (getRule() != null) {
				LOGGER.debug("Evaluating rule [" + getName() + "] (start)");
				session.setVariables(getArgs());
				result = getRule().eval(context, session, message);
				LOGGER.debug("Evaluating rule [" + getName() + "] (" + result + ")");
				LOGGER.debug("Evaluating rule [" + getName() + "] (stop)");
			}
			if (result == true) {
				for (IRule action : getActions()) {
					result = action.eval(context, session, message);
					if (isMatchFirst() == true) {
						if (result == true) {
							LOGGER.info("Evaluating rule [" + getName() + "] (matched)");
							break;
						} else {
							LOGGER.info("Evaluating rule [" + getName() + "] (not matched)");
						}
					}
				}
			}
			LOGGER.info("Evaluating rule [" + getName() + "] (stop)");
			return result;
		}
	}
}
